/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright 2003-2005 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack;

import javax.net.ssl.X509TrustManager;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Trust manager that checks all certificates presented by the server. This
 * class is used during TLS negotiation. It is possible to disable/enable some
 * or all checkings by configuring the {@link ConnectionConfiguration}. The
 * truststore file that contains knows and trusted CA root certificates can also
 * be configure in {@link ConnectionConfiguration}.
 * 
 * @author Gaston Dombiak
 */
class ServerTrustManager implements X509TrustManager {

	private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");

	private ConnectionConfiguration configuration;

	/**
	 * Holds the domain of the remote server we are trying to connect
	 */
	private String server;
	private KeyStore trustStore;

	private static Map<KeyStoreOptions, KeyStore> stores = new HashMap<KeyStoreOptions, KeyStore>();

	public ServerTrustManager(String server,
			ConnectionConfiguration configuration) {
		this.configuration = configuration;
		this.server = server;

		InputStream in = null;
		synchronized (stores) {
			KeyStoreOptions options = new KeyStoreOptions(
					configuration.getTruststoreType(),
					configuration.getTruststorePath(),
					configuration.getTruststorePassword());
			if (stores.containsKey(options)) {
				trustStore = stores.get(options);
			} else {
				try {
					trustStore = KeyStore.getInstance(options.getType());
					if (options.getPath() != null) {
						in = new BufferedInputStream(new FileInputStream(
								options.getPath()));
					}
					char[] chars = null;
					if (options.getPassword() != null) {
						chars = options.getPassword().toCharArray();
					}
					trustStore.load(in, chars);
				} catch (Exception e) {
					trustStore = null;
					e.printStackTrace();
				} finally {
					if (in != null) {
						try {
							in.close();
						} catch (IOException ioe) {
							// Ignore.
						}
					}
				}
				stores.put(options, trustStore);
			}
			if (trustStore == null)
				// Disable root CA checking
				configuration.setVerifyRootCAEnabled(false);
		}
	}

	public X509Certificate[] getAcceptedIssuers() {
		return new X509Certificate[0];
	}

	public void checkClientTrusted(X509Certificate[] arg0, String arg1)
			throws CertificateException {
	}

	public void checkServerTrusted(X509Certificate[] x509Certificates,
			String arg1) throws CertificateException {

		int nSize = x509Certificates.length;

		List<String> peerIdentities = getPeerIdentity(x509Certificates[0]);

		if (configuration.isVerifyChainEnabled()) {
			// Working down the chain, for every certificate in the chain,
			// verify that the subject of the certificate is the issuer of the
			// next certificate in the chain.
			Principal principalLast = null;
			for (int i = nSize - 1; i >= 0; i--) {
				X509Certificate x509certificate = x509Certificates[i];
				Principal principalIssuer = x509certificate.getIssuerDN();
				Principal principalSubject = x509certificate.getSubjectDN();
				if (principalLast != null) {
					if (principalIssuer.equals(principalLast)) {
						try {
							PublicKey publickey = x509Certificates[i + 1]
									.getPublicKey();
							x509Certificates[i].verify(publickey);
						} catch (GeneralSecurityException generalsecurityexception) {
							throw new CertificateException(
									"signature verification failed of "
											+ peerIdentities);
						}
					} else {
						throw new CertificateException(
								"subject/issuer verification failed of "
										+ peerIdentities);
					}
				}
				principalLast = principalSubject;
			}
		}

		if (configuration.isVerifyRootCAEnabled()) {
			// Verify that the the last certificate in the chain was issued
			// by a third-party that the client trusts.
			boolean trusted = false;
			try {
				trusted = trustStore
						.getCertificateAlias(x509Certificates[nSize - 1]) != null;
				if (!trusted && nSize == 1
						&& configuration.isSelfSignedCertificateEnabled()) {
					System.out
							.println("Accepting self-signed certificate of remote server: "
									+ peerIdentities);
					trusted = true;
				}
			} catch (KeyStoreException e) {
				e.printStackTrace();
			}
			if (!trusted) {
				throw new CertificateException(
						"root certificate not trusted of " + peerIdentities);
			}
		}

		if (configuration.isNotMatchingDomainCheckEnabled()) {
			// Verify that the first certificate in the chain corresponds to
			// the server we desire to authenticate.
			// Check if the certificate uses a wildcard indicating that
			// subdomains are valid
			if (peerIdentities.size() == 1
					&& peerIdentities.get(0).startsWith("*.")) {
				// Remove the wildcard
				String peerIdentity = peerIdentities.get(0).replace("*.", "");
				// Check if the requested subdomain matches the certified domain
				if (!server.endsWith(peerIdentity)) {
					throw new CertificateException(
							"target verification failed of " + peerIdentities);
				}
			} else if (!peerIdentities.contains(server)) {
				throw new CertificateException("target verification failed of "
						+ peerIdentities);
			}
		}

		if (configuration.isExpiredCertificatesCheckEnabled()) {
			// For every certificate in the chain, verify that the certificate
			// is valid at the current time.
			Date date = new Date();
			for (int i = 0; i < nSize; i++) {
				try {
					x509Certificates[i].checkValidity(date);
				} catch (GeneralSecurityException generalsecurityexception) {
					throw new CertificateException("invalid date of " + server);
				}
			}
		}

	}

	/**
	 * Returns the identity of the remote server as defined in the specified
	 * certificate. The identity is defined in the subjectDN of the certificate
	 * and it can also be defined in the subjectAltName extension of type
	 * "xmpp". When the extension is being used then the identity defined in the
	 * extension in going to be returned. Otherwise, the value stored in the
	 * subjectDN is returned.
	 * 
	 * @param x509Certificate
	 *            the certificate the holds the identity of the remote server.
	 * @return the identity of the remote server as defined in the specified
	 *         certificate.
	 */
	public static List<String> getPeerIdentity(X509Certificate x509Certificate) {
		// Look the identity in the subjectAltName extension if available
		List<String> names = getSubjectAlternativeNames(x509Certificate);
		if (names.isEmpty()) {
			String name = x509Certificate.getSubjectDN().getName();
			Matcher matcher = cnPattern.matcher(name);
			if (matcher.find()) {
				name = matcher.group(2);
			}
			// Create an array with the unique identity
			names = new ArrayList<String>();
			names.add(name);
		}
		return names;
	}

	/**
	 * Returns the JID representation of an XMPP entity contained as a
	 * SubjectAltName extension in the certificate. If none was found then
	 * return <tt>null</tt>.
	 * 
	 * @param certificate
	 *            the certificate presented by the remote entity.
	 * @return the JID representation of an XMPP entity contained as a
	 *         SubjectAltName extension in the certificate. If none was found
	 *         then return <tt>null</tt>.
	 */
	private static List<String> getSubjectAlternativeNames(
			X509Certificate certificate) {
		List<String> identities = new ArrayList<String>();
		try {
			Collection<List<?>> altNames = certificate
					.getSubjectAlternativeNames();
			// Check that the certificate includes the SubjectAltName extension
			if (altNames == null) {
				return Collections.emptyList();
			}
			// Use the type OtherName to search for the certified server name
			/*
			 * for (List item : altNames) { Integer type = (Integer)
			 * item.get(0); if (type == 0) { // Type OtherName found so return
			 * the associated value try { // Value is encoded using ASN.1 so
			 * decode it to get the server's identity ASN1InputStream decoder =
			 * new ASN1InputStream((byte[]) item.toArray()[1]); DEREncodable
			 * encoded = decoder.readObject(); encoded = ((DERSequence)
			 * encoded).getObjectAt(1); encoded = ((DERTaggedObject)
			 * encoded).getObject(); encoded = ((DERTaggedObject)
			 * encoded).getObject(); String identity = ((DERUTF8String)
			 * encoded).getString(); // Add the decoded server name to the list
			 * of identities identities.add(identity); } catch
			 * (UnsupportedEncodingException e) { // Ignore } catch (IOException
			 * e) { // Ignore } catch (Exception e) { e.printStackTrace(); } }
			 * // Other types are not good for XMPP so ignore them
			 * System.out.println("SubjectAltName of invalid type found: " +
			 * certificate); }
			 */
		} catch (CertificateParsingException e) {
			e.printStackTrace();
		}
		return identities;
	}

	private static class KeyStoreOptions {
		private final String type;
		private final String path;
		private final String password;

		public KeyStoreOptions(String type, String path, String password) {
			super();
			this.type = type;
			this.path = path;
			this.password = password;
		}

		public String getType() {
			return type;
		}

		public String getPath() {
			return path;
		}

		public String getPassword() {
			return password;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result
					+ ((password == null) ? 0 : password.hashCode());
			result = prime * result + ((path == null) ? 0 : path.hashCode());
			result = prime * result + ((type == null) ? 0 : type.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			KeyStoreOptions other = (KeyStoreOptions) obj;
			if (password == null) {
				if (other.password != null)
					return false;
			} else if (!password.equals(other.password))
				return false;
			if (path == null) {
				if (other.path != null)
					return false;
			} else if (!path.equals(other.path))
				return false;
			if (type == null) {
				if (other.type != null)
					return false;
			} else if (!type.equals(other.type))
				return false;
			return true;
		}
	}

}
